package com.lemeshko.xml;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

public class BooksParser {

	public static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
	public static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
	private static final SimpleDateFormat dateFormat = new SimpleDateFormat(
			"yyyy-mm-dd");

	public static List<Book> parseBooksXml(String url)
			throws ParserConfigurationException, SAXException, IOException,
			ParseException {

		Document doc = getDocument(url);

		Element root = doc.getDocumentElement();
		NodeList children = root.getChildNodes();

		List<Book> books = new ArrayList<Book>();

		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);

			if (((Element) child).getTagName().equals("books")) {
				NodeList booksList = child.getChildNodes();
				for (int k = 0; k < booksList.getLength(); k++) {
					Node node = booksList.item(k);
					if (((Element) node).getTagName().equals("book")) {
						books.add(parseBookXmlElement((Element) node));
					}
				}

				break;
			}
		}

		return books;
	}

	public static Document getDocument(String url)
			throws ParserConfigurationException, SAXException, IOException {

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(true);
		factory.setNamespaceAware(true);
		factory.setAttribute(JAXP_SCHEMA_LANGUAGE,
				XMLConstants.W3C_XML_SCHEMA_NS_URI);
		factory.setAttribute(JAXP_SCHEMA_SOURCE, "books.xsd");
		factory.setIgnoringElementContentWhitespace(true);

		DocumentBuilder builder = factory.newDocumentBuilder();
		builder.setErrorHandler(new ErrorHandler() {

			@Override
			public void warning(SAXParseException warning) throws SAXException {
				throw warning;
			}

			@Override
			public void fatalError(SAXParseException error) throws SAXException {
				throw error;
			}

			@Override
			public void error(SAXParseException error) throws SAXException {
				throw error;
			}
		});

		return builder.parse(url);
	}

	protected static Book parseBookXmlElement(Element bookNode)
			throws ParseException {

		int isbn = 0;
		String name = null;
		String description = null;
		Date publishDate = null;
		List<String> authors = null;

		int bookId = Integer.parseInt(bookNode.getAttribute("id"));
		NodeList nodes = bookNode.getChildNodes();

		for (int i = 0; i < nodes.getLength(); i++) {
			Node node = nodes.item(i);

			Element childElement = (Element) node;
			String tagName = childElement.getTagName();

			if (tagName.equals("authors"))
				authors = parseAuthors(childElement);
			else {
				Text textNode = (Text) childElement.getFirstChild();
				String text = textNode.getData().trim();

				if (tagName.equals("ISBN"))
					isbn = Integer.parseInt(text);
				else if (tagName.equals("name"))
					name = text;
				else if (tagName.equals("description"))
					description = text;
				else if (tagName.equals("publishDate"))
					publishDate = dateFormat.parse(text);
			}
		}

		return new Book(bookId, isbn, name, description, publishDate, authors);
	}

	protected static List<String> parseAuthors(Element authorsElement) {

		List<String> authors = new ArrayList<String>();

		NodeList authorsList = authorsElement.getChildNodes();
		for (int i = 0; i < authorsList.getLength(); i++) {
			Node node = authorsList.item(i);

			Element authorElement = (Element) node;
			Text text = (Text) authorElement.getFirstChild();
			authors.add(text.getData().trim());
		}

		return authors;

	}

	public static List<Book> parseBooksXmlByXPath(String url)
			throws ParserConfigurationException, SAXException, IOException,
			XPathExpressionException, ParseException {

		List<Book> books = new ArrayList<Book>();

		Document doc = getDocument(url);

		XPathFactory factory = XPathFactory.newInstance();
		XPath path = factory.newXPath();

		int booksNodesCount = ((Number) path.evaluate(
				"count(/catalog/books/book)", doc, XPathConstants.NUMBER))
				.intValue();

		for (int i = 1; i <= booksNodesCount; i++) {
			Node bookNode = (Node) path.evaluate("/catalog/books/book[" + i
					+ "]", doc, XPathConstants.NODE);

			books.add(parseBookNodeByXPath(path, bookNode));
		}

		return books;
	}

	protected static Book parseBookNodeByXPath(XPath path, Node bookNode)
			throws XPathExpressionException, ParseException {

		int bookId = Integer.parseInt(path.evaluate("@id", bookNode));

		String name = path.evaluate("name", bookNode);
		int isbn = Integer.parseInt(path.evaluate("ISBN", bookNode));
		String description = path.evaluate("description", bookNode);
		Date pDate = dateFormat.parse(path.evaluate("publishDate", bookNode));

		return new Book(bookId, isbn, name, description, pDate,
				parseAuthorsByXPath(path, bookNode));

	}

	protected static List<String> parseAuthorsByXPath(XPath path,
			Node authorsNode) throws XPathExpressionException {

		List<String> authors = new ArrayList<String>();

		int authorsCount = ((Number) path.evaluate("count(authors/name)",
				authorsNode, XPathConstants.NUMBER)).intValue();

		for (int i = 1; i <= authorsCount; i++) {
			authors.add(path.evaluate("authors/name[" + i + "]", authorsNode));
		}

		return authors;
	}
}
